<?php

class Am_Paysystem_AuthorizeCim extends Am_Paysystem_CreditCard
{
    const PLUGIN_STATUS = self::STATUS_BETA;
    const PLUGIN_DATE = '$Date$';
    const PLUGIN_REVISION = '$Revision$';

    const LIVE_URL = 'https://api.authorize.net/xml/v1/request.api';
    const SANDBOX_URL = 'https://apitest.authorize.net/xml/v1/request.api';
    
    const MERCHANT_CUSTOMER_ID_KEY = 'cim-merchant-customer-id-';
    const USER_PROFILE_KEY = 'authorize_cim_user_profile_id';
    const PAYMENT_PROFILE_KEY = 'authorize_cim_payment_profile_id';

    protected $defaultTitle = "Pay with your Credit Card";
    protected $defaultDescription  = "accepts all major credit cards";
    
    public function getRecurringType()
    {
        return self::REPORTS_REBILL;
    }
    
    public function getSupportedCurrencies()
    {
        return array('USD', 'EUR', 'GBP', 'CAD');
    }
    
    public function isConfigured()
    {
        return strlen($this->getConfig('login')) && strlen($this->getConfig('tkey'));
    }
    public function _doBill(Invoice $invoice, $doFirst, CcRecord $cc, Am_Paysystem_Result $result)
    {
        $user = $invoice->getUser();
        if ($cc->user_id != $user->pk())
            throw new Am_Exception_Paysystem("Assertion failed: cc.user_id != user.user_id");
        
        // will be stored only if cc# or expiration changed
        $this->storeCreditCard($cc, $result);
        if (!$result->isSuccess())
            return;

        $user->refresh();
        $profileId = $user->data()->get(Am_Paysystem_AuthorizeCim::USER_PROFILE_KEY);
        $payProfileId = $user->data()->get(self::PAYMENT_PROFILE_KEY);
        
        // we have both profile id and payment id, run the necessary transaction now if amount > 0
        $result->reset();
        
        if ($doFirst && !$invoice->first_total)
        { // free trial
            $tr = new Am_Paysystem_Transaction_Free($this);
            $tr->setInvoice($invoice);
            $tr->process();
            $result->setSuccess($tr);
        } else {
            $tr = new Am_Paysystem_Transaction_AuthorizeCim_CreateCustomerProfileTransaction($this, $invoice, $doFirst);
            $tr->run($result);
        }
    }
    
    public function _initSetupForm(Am_Form_Setup $form)
    {
        $form->addText("login")->setLabel(array('API Login ID', '
            can be obtained from the same page as Transaction Key (see below)'));
        $form->addText("tkey")->setLabel(array('Transaction Key', 
            "
The transaction key is generated by the system 
and can be obtained from Merchant Interface.
To obtain the transaction key from the Merchant
Interface:
  *  Log into the Merchant Interface
  *  Select [Settings] from the Main Menu
  *  Click on [API Login ID and Transaction Key] in the Security section
  *  Type in the answer to the secret question configured on setup
  *  Click Submit
"));
        $form->addAdvCheckbox("testing")->setLabel("Test Mode Enabled");
        
    }
    
    public function getMerchantCustomerId($userOrId)
    {
        if ($userOrId instanceof User) $userOrId = $userOrId->pk();
        return $userOrId . '-' . substr($this->getDi()->app->getSiteHash('cim'), 0, 5);
    }
    
    public function storeCreditCard(CcRecord $cc, Am_Paysystem_Result $result)
    {
        
        
        $user = $this->getDi()->userTable->load($cc->user_id);
        $profileId = $user->data()->get(Am_Paysystem_AuthorizeCim::USER_PROFILE_KEY);
        if ($this->invoice)
        { // to link log records with current invoice
            $invoice = $this->invoice;
        } else { // updating credit card info?
            $invoice = $this->getDi()->invoiceRecord;
            $invoice->invoice_id = 0;
            $invoice->user_id = $user->pk();
        }

        // compare stored cc for that user may be we don't need to refresh?
        if ($profileId && ($cc->cc_number != '0000000000000000'))
        {
            $storedCc = $this->getDi()->ccRecordTable->findFirstByUserId($user->pk());
            if ($storedCc && (($storedCc->cc != $cc->maskCc($cc->cc_number)) || ($storedCc->cc_expire != $cc->cc_expire)))
            {
                $user->data()
                    ->set(self::USER_PROFILE_KEY, null)
                    ->set(self::PAYMENT_PROFILE_KEY, null)
                    ->update();
                $deleteTr = new Am_Paysystem_Transaction_AuthorizeCim_DeleteCustomerProfile($this, $invoice, $profileId);
                $deleteTr->run($res = new Am_Paysystem_Result);
                $profileId = null;
            }
        }
        
        if (!$profileId)
        {
            try {
                $tr = new Am_Paysystem_Transaction_AuthorizeCim_CreateCustomerProfile($this, $invoice, $cc);
                $tr->run($result);
                if (!$result->isSuccess())
                    return;
                $user->data()->set(Am_Paysystem_AuthorizeCim::USER_PROFILE_KEY, $tr->getProfileId())->update();
                $user->data()->set(Am_Paysystem_AuthorizeCim::PAYMENT_PROFILE_KEY, $tr->getPaymentId())->update();
            } catch (Am_Exception_Paysystem $e) {
                $result->setFailed($e->getPublicError());
                return false;
            }
        }
        /// 
        $cc->cc = $cc->maskCc(@$cc->cc_number);
        $cc->cc_number = '0000000000000000';
        if ($cc->pk())
            $cc->update();
        else
            $cc->replace();
        $result->setSuccess();
    }
    
    public function getReadme()
    {
        return <<<CUT
Authorize.Net CIM 
---------------------------------------------------
  
The biggest advantage of this plugin is that altough credit card info
is entered on your website, it will be stored on Auth.Net secure servers
so recurring billing is secure and you do not have to store cc info on your
own website.

You need to enable CIM service in authorize.net 
(Tools -> Customer Information Manager -> Sign Up Now)
This is a paid service.
             

1. Enable and configure plugin in aMember CP -> Setup -> Plugins

2. You NEED to use external cron with this plugins
    (See Setup/Configuration -> Advanced)

CUT;
    }
}

abstract class Am_Paysystem_Transaction_AuthorizeCim extends Am_Paysystem_Transaction_CreditCard
{
    protected $apiName = null;
    protected $aimResponse = null;
    
    public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $doFirst = true)
    {
        parent::__construct($plugin, $invoice, $plugin->createHttpRequest(), $doFirst);
        $this->request->setHeader('Content-type', 'text/xml');
        $this->request->setBody($this->createXml($this->apiName)->asXml());
        $this->request->setMethod(Am_HttpRequest::METHOD_POST);
        $this->request->setUrl(!$this->plugin->getConfig('testing') ? 
            Am_Paysystem_AuthorizeCim::LIVE_URL : 
            Am_Paysystem_AuthorizeCim::SANDBOX_URL);
    }
    /** @return SimpleXmlElement */
    protected function createXml($name)
    {
        $xml = new SimpleXmlElement('<'.$name.'/>');
        $xml['xmlns'] = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd';
        $xml->merchantAuthentication->name = $this->plugin->getConfig('login');
        $xml->merchantAuthentication->transactionKey = $this->plugin->getConfig('tkey');
        return $xml;
    }
    public function parseResponse()
    {
        $body = trim($this->response->getBody());
        $body = str_replace('xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd"', '', $body);
        $this->xml = new SimpleXMLElement($body);
    }
    public function parseAimString($string)
    {
        $vars = explode(',', $string);
        $this->aimResponse = new stdclass;
        if (count($vars) < 10) 
        {
            $this->aimResponse->error_message = "Unrecognized response from Authorize.Net: " . $string;
            return false;
        }
        // Set all fields
        $this->aimResponse->aimResponse_code        = $vars[0];
        $this->aimResponse->aimResponse_subcode     = $vars[1];
        $this->aimResponse->aimResponse_reason_code = $vars[2];
        $this->aimResponse->aimResponse_reason_text = $vars[3];
        $this->aimResponse->authorization_code   = $vars[4];
        $this->aimResponse->avs_response         = $vars[5];
        $this->aimResponse->transaction_id       = $vars[6];
        $this->aimResponse->invoice_number       = $vars[7];
        $this->aimResponse->description          = $vars[8];
        $this->aimResponse->amount               = $vars[9];
        $this->aimResponse->method               = $vars[10];
        $this->aimResponse->transaction_type     = $vars[11];
        $this->aimResponse->customer_id          = $vars[12];
        $this->aimResponse->first_name           = $vars[13];
        $this->aimResponse->last_name            = $vars[14];
        $this->aimResponse->company              = $vars[15];
        $this->aimResponse->address              = $vars[16];
        $this->aimResponse->city                 = $vars[17];
        $this->aimResponse->state                = $vars[18];
        $this->aimResponse->zip_code             = $vars[19];
        $this->aimResponse->country              = $vars[20];
        $this->aimResponse->phone                = $vars[21];
        $this->aimResponse->fax                  = $vars[22];
        $this->aimResponse->email_address        = $vars[23];
        $this->aimResponse->ship_to_first_name   = $vars[24];
        $this->aimResponse->ship_to_last_name    = $vars[25];
        $this->aimResponse->ship_to_company      = $vars[26];
        $this->aimResponse->ship_to_address      = $vars[27];
        $this->aimResponse->ship_to_city         = $vars[28];
        $this->aimResponse->ship_to_state        = $vars[29];
        $this->aimResponse->ship_to_zip_code     = $vars[30];
        $this->aimResponse->ship_to_country      = $vars[31];
        $this->aimResponse->tax                  = $vars[32];
        $this->aimResponse->duty                 = $vars[33];
        $this->aimResponse->freight              = $vars[34];
        $this->aimResponse->tax_exempt           = $vars[35];
        $this->aimResponse->purchase_order_number= $vars[36];
        $this->aimResponse->md5_hash             = $vars[37];
        $this->aimResponse->card_code_response   = $vars[38];
        $this->aimResponse->cavv_response        = $vars[39];
        $this->aimResponse->account_number       = $vars[40];
        $this->aimResponse->card_type            = $vars[51];
        $this->aimResponse->split_tender_id      = $vars[52];
        $this->aimResponse->requested_amount     = $vars[53];
        $this->aimResponse->balance_on_card      = $vars[54];
        return true;
    }
    
}


class Am_Paysystem_Transaction_AuthorizeCim_CreateCustomerProfile extends Am_Paysystem_Transaction_AuthorizeCim
{
    protected $apiName = 'createCustomerProfileRequest';
    /** @var CcRecord */
    protected $cc;
    
    public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, CcRecord $cc)
    {
        $this->cc = $cc;
        parent::__construct($plugin, $invoice);
    }
    
    protected function createXml($name)
    {
        $xml = parent::createXml($name);
        $user = $this->invoice->getUser();
        $xml->profile->merchantCustomerId = $this->plugin->getMerchantCustomerId($this->cc->user_id);
        $xml->profile->description = "Username: $user->login";
        $xml->profile->email = $user->email;
        $xml->profile->paymentProfiles->billTo->firstName = $this->cc->cc_name_f;
        $xml->profile->paymentProfiles->billTo->lastName = $this->cc->cc_name_l;
        $xml->profile->paymentProfiles->billTo->address = $this->cc->cc_street;
        $xml->profile->paymentProfiles->billTo->city = $this->cc->cc_city;
        $xml->profile->paymentProfiles->billTo->state = $this->cc->cc_state;
        $xml->profile->paymentProfiles->billTo->zip = $this->cc->cc_zip;
        $xml->profile->paymentProfiles->billTo->country = $this->cc->cc_country;
        $xml->profile->paymentProfiles->billTo->phoneNumber = $this->cc->cc_phone;
        $xml->profile->paymentProfiles->payment->creditCard->cardNumber = $this->cc->cc_number;
        $xml->profile->paymentProfiles->payment->creditCard->expirationDate = $this->cc->getExpire('20%2$02d-%1$02d');
        if (strlen($this->cc->getCvv()))
            $xml->profile->paymentProfiles->payment->creditCard->cardCode = $this->cc->getCvv();
        $xml->validationMode = 'liveMode';
        return $xml;
    }
    public function validate()
    {
        if ($this->xml->getName() != 'createCustomerProfileResponse')
        {
            $this->result->setFailed(___('Payment failed'));
            return;
        }
        if ((string)$this->xml->messages->message->code != 'I00001')
        {
            $this->result->setFailed((string)$this->xml->messages->message->text);
            return;
        }
        $this->result->setSuccess();
        return true;
    }
    function getProfileId()
    {
        return (string)$this->xml->customerProfileId;
    }
    function getPaymentId()
    {
        return (string)$this->xml->customerPaymentProfileIdList->numericString;
    }
    public function getUniqId()
    {
        return (string)$this->xml->customerProfileId;
    }
    public function processValidated()
    {
    }
}
    
class Am_Paysystem_Transaction_AuthorizeCim_DeleteCustomerProfile extends Am_Paysystem_Transaction_AuthorizeCim
{
    protected $apiName = 'deleteCustomerProfileRequest';
    protected $profileId;
    public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $profileId)
    {
        $this->profileId = $profileId;
        parent::__construct($plugin, $invoice, true);
    }
    protected function createXml($name)
    {
        $xml = parent::createXml($name);
        $xml->customerProfileId = $this->profileId;
        return $xml;
    }
    public function validate()
    {
        if ($this->xml->getName() != 'deleteCustomerProfileResponse')
        {
            $this->result->setFailed(___('Profile update transaction failed'));
            return;
        }
        if ((string)$this->xml->messages->message->code != 'I00001')
        {
            $this->result->setFailed((string)$this->xml->messages->message->text);
            return;
        }
        $this->result->setSuccess();
        return true;
    }
    public function getUniqId()
    {
        return uniqid();
    }
    public function processValidated()
    {
    }
}

class Am_Paysystem_Transaction_AuthorizeCim_CreateCustomerProfileTransaction extends Am_Paysystem_Transaction_AuthorizeCim
{
    protected $apiName = 'createCustomerProfileTransactionRequest';
    /** @var object */
    protected $aimResponse;
    
    protected function createXml($name)
    {
        $xml = parent::createXml($name);
        $xml->transaction->profileTransAuthCapture->amount = 
            $this->doFirst ? $this->invoice->first_total : $this->invoice->second_total;
        $xml->transaction->profileTransAuthCapture->tax->amount = 
            $this->doFirst ? $this->invoice->first_tax : $this->invoice->second_tax;
        $xml->transaction->profileTransAuthCapture->shipping->amount = 
            $this->doFirst ? $this->invoice->first_shipping : $this->invoice->second_shipping;
        
        foreach ($this->invoice->getItems() as $item)
        {
            /* @var $item InvoiceItem */
            $line = $xml->transaction->profileTransAuthCapture->addChild('lineItems');
            $line->itemId = $item->item_id;
            $line->name = substr($item->item_title, 0, 30);
            $line->quantity = $item->qty;
            $price = $this->doFirst ? $item->first_price : $item->second_price;
            if ($price)
                $line->unitPrice = $price;
            if ($this->doFirst ? $item->first_tax : $item->second_tax)
                $line->taxable = 'true';
            else
                $line->taxable = 'false';
        }

        $user = $this->invoice->getUser();
        
        $taxAmount = $this->doFirst ? $this->invoice->first_tax : $this->invoice->second_tax;
        if ($taxAmount)
        $xml->transaction->profileTransAuthCapture->tax->amount = $taxAmount;

        $xml->transaction->profileTransAuthCapture->customerProfileId =
            $user->data()->get(Am_Paysystem_AuthorizeCim::USER_PROFILE_KEY);
        $xml->transaction->profileTransAuthCapture->customerPaymentProfileId =
            $user->data()->get(Am_Paysystem_AuthorizeCim::PAYMENT_PROFILE_KEY);
        $xml->transaction->profileTransAuthCapture->order->description =
            $this->invoice->getLineDescription();
        $xml->transaction->profileTransAuthCapture->order->purchaseOrderNumber = 
            $this->invoice->public_id . '-' . $this->invoice->getPaymentsCount();
        $xml->transaction->profileTransAuthCapture->recurringBilling =
            $this->doFirst ? 'true' : 'false';
        return $xml;
    }
    public function validate()
    {
        if ($this->xml->getName() != 'createCustomerProfileTransactionResponse')
        {
            $this->result->setFailed(___('Payment failed'));
            return;
        }
        if (!$this->parseAimString($this->xml->directResponse))
        {
            $this->result->setFailed(___('Payment failed'));
            return;
        }
        if ((string)$this->xml->messages->message->code != 'I00001')
        {
            $this->result->setFailed(___('Payment failed'));
            return;
        }
        $this->result->setSuccess($this);
        return true;
    }
    public function getUniqId()
    {
        return $this->aimResponse->transaction_id;
    }
    public function processValidated()
    {
        $this->invoice->addPayment($this);
    }
}
